tractatus/public/js/i18n-simple.js
TheFlow ce7747175c feat(compliance): add GDPR compliance page with trilingual support
Implements comprehensive GDPR compliance documentation explaining how the
Tractatus Framework enforces data protection through architectural constraints
rather than policy documents.

Key features:
- 8 sections covering GDPR Articles 5, 6, 15-22, 25, 32, 33
- Framework positioning: BoundaryEnforcer, CrossReferenceValidator, PluralisticDeliberationOrchestrator
- Full trilingual support (EN/DE/FR) via DeepL API (322 translations)
- Footer links and i18n integration across all languages
- Professional translations for legal accuracy

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

Co-Authored-By: Claude <noreply@anthropic.com>
2025-10-28 10:26:57 +13:00

270 lines
8.3 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();
// 5. Dispatch initialization complete event
window.dispatchEvent(new CustomEvent('i18nInitialized', {
detail: { language: this.currentLang }
}));
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',
'/gdpr.html': 'gdpr',
'/gdpr': 'gdpr',
'/blog.html': 'blog',
'/blog.html': 'blog',
'/blog': 'blog',
'/architecture.html': 'architecture',
'/architecture': 'architecture'
};
return pageMap[path] || 'homepage';
},
async loadTranslations(lang) {
try {
// Cache-busting: Add timestamp to force fresh fetch and bypass stale cache
const cacheBust = `?v=${Date.now()}`;
// Always load common translations (footer, navbar, etc.)
const commonResponse = await fetch(`/locales/${lang}/common.json${cacheBust}`);
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${cacheBust}`);
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}`);
}
// Deep merge common and page-specific translations (page-specific takes precedence)
// Uses deep merge to preserve nested objects like footer in common.json
this.translations = this.deepMerge(commonTranslations, pageTranslations);
// Expose translations globally for components like interactive-diagram
window.i18nTranslations = this.translations;
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;
},
/**
* Deep merge utility function
* Recursively merges source into target without overwriting nested objects
* @param {Object} target - Base object (e.g., commonTranslations)
* @param {Object} source - Override object (e.g., pageTranslations)
* @returns {Object} - Deeply merged result
*/
deepMerge(target, source) {
const output = { ...target };
for (const key in source) {
if (source.hasOwnProperty(key)) {
// If both values are objects (and not arrays), merge recursively
if (source[key] && typeof source[key] === 'object' && !Array.isArray(source[key]) &&
target[key] && typeof target[key] === 'object' && !Array.isArray(target[key])) {
output[key] = this.deepMerge(target[key], source[key]);
} else {
// Otherwise, source value takes precedence
output[key] = source[key];
}
}
}
return output;
}
};
// Auto-initialize when DOM is ready
if (document.readyState === 'loading') {
document.addEventListener('DOMContentLoaded', () => I18n.init());
} else {
I18n.init();
}
// Make available globally
window.I18n = I18n;