Case Submission Portal (Admin Moderation Queue): - Add statistics endpoint (GET /api/cases/submissions/stats) - Enhance filtering: status, failure_mode, AI relevance score - Add sorting options: date, relevance, completeness - Create admin moderation interface (case-moderation.html) - Implement CSP-compliant admin UI (no inline event handlers) - Deploy moderation actions: approve, reject, request-info - Fix API parameter mapping for different action types Internationalization (i18n): - Implement lightweight i18n system (i18n-simple.js, ~5KB) - Add language selector component with flag emojis - Create German and French translations for homepage - Document Te Reo Māori translation requirements - Add i18n attributes to homepage - Integrate language selector into navbar Bug Fixes: - Fix search button modal display on docs.html (remove conflicting flex class) Page Enhancements: - Add dedicated JS modules for researcher, leader, koha pages - Improve page-specific functionality and interactions Documentation: - Add I18N_IMPLEMENTATION_SUMMARY.md (implementation guide) - Add TE_REO_MAORI_TRANSLATION_REQUIREMENTS.md (cultural sensitivity guide) 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude <noreply@anthropic.com>
151 lines
3.7 KiB
JavaScript
151 lines
3.7 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;
|
|
|
|
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;
|