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>
270 lines
8.3 KiB
JavaScript
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;
|